#!/usr/bin/env python3

"""
This file tests src.get_roll.
Three main parts are tested:
general form, injection, and failure detection.
"""

import unittest
from src.gen_roll import get_roll
from src.config import DICE, HITS, FAILS

class TestRoll(unittest.TestCase):
    """Unittest Testcase for src.get_roll"""
    # neutral are all dice results minux the results of HITS and FAILS
    NEUTRAL_RES = set(DICE) - set(HITS) - set(FAILS)
    NEUTRAL_RES = list(NEUTRAL_RES)
    NON_FAILS = NEUTRAL_RES + HITS

    # general form

    def test_normal_length(self):
        """Tests if the result length is equal to the amount passed."""
        len_roll_result = len(get_roll(3)["result"])
        self.assertEqual(len_roll_result, 3, "amount of dice throws should be equal to results")

    def test_available_keys(self):
        """Tests if different amount roll dicts have all the necessary key"""
        keys = ["result", "hits", "fails", "isFailure"]
        rolls = [get_roll(1), get_roll(2), get_roll(3)]
        for roll in rolls:
            has_all_keys = True
            for key in keys:
                if not key in roll:
                    has_all_keys = False
            self.assertTrue(has_all_keys,
                    f"roll dict with length {len(roll['result'])} should have all keys")

    # injection tests

    def test_injection(self):
        """Tests if the injection is the same in the output"""
        if len(DICE) < 2:
            print("Unable to test injection ordering, since too few elements exist")
            return
        injections = _get_ordered_injection(DICE, [0,1,2,3])
        for injection in injections:
            roll = get_roll(len(injection), injection)
            self.assertEqual(roll["result"], injection,
                    f"injection {injection} should be the same in the roll result")

    def test_injection_fill(self):
        """Tests if an underfull injection gets filled up by random numbers"""
        injections = _get_ordered_injection(DICE, [0,1,2])
        amount = 4
        for injection in injections:
            roll = get_roll(amount, injection)
            self.assertEqual(amount, len(roll["result"]),
                    f"injected roll result {injection} length should be equal to amount")

    def test_injection_overlong(self):
        """Tests if an overlong injection will be cut off"""
        injection = _get_ordered_injection(DICE, [5])[0]
        amount = 4
        roll = get_roll(amount, injection)
        self.assertEqual(roll["result"], injection[:amount],
                "injected overlong roll should be equal after being cut off to the result")

    def test_injection_fill_placement(self):
        """Tests if a injected underfull injection is correctly placed"""
        injections = _get_ordered_injection(DICE, [0,1,2])
        amount = 4
        for injection in injections:
            roll = get_roll(amount, injection)
            res = roll["result"][:len(injection)]
            self.assertEqual(res, injection,
                    "beginning of result should be equal to underlong injection")

    # failure tests

    def test_failure_detection(self):
        """Tests if failures are detected correctly.
        Failures are if half (rounded up) or more are failure rolls (here: 1)."""
        fail = FAILS[0]
        non_fail = self.NON_FAILS[0]
        if len(FAILS) < 1 and len(self.NON_FAILS) < 1:
            print("Not able to test failure detection with no failures or no neutral results/hits")
            return
        # test fail
        roll = get_roll(1, [fail])
        self.assertTrue(roll["isFailure"],
                "single roll with result {fail} should be a failure according to config")
        # test non fail
        for res in self.NON_FAILS:
            roll = get_roll(1, [res])
            self.assertFalse(roll["isFailure"],
                    f"single roll with result {res} should not be a failure according to config")
        # test uneven
        for res in [[fail,non_fail,non_fail], [non_fail]*3]:
            roll = get_roll(3, res)
            self.assertFalse(roll["isFailure"],
                    f"tripple roll with result {res} should not be a failure")
        for res in [[fail]*3, [fail,fail,non_fail]]:
            roll = get_roll(3, res)
            self.assertTrue(roll["isFailure"],
                    f"tripple roll with result {res} should be a failure")
        # test uneven
        for res in [[fail,fail,non_fail,non_fail], [non_fail]*4]:
            roll = get_roll(4, res)
            self.assertFalse(roll["isFailure"],
                    f"quad roll with result {res} should not be a failure")
        for res in [[fail]*4, [fail,fail,fail,non_fail]]:
            roll = get_roll(4, res)
            self.assertTrue(roll["isFailure"],
                    f"quad roll with result {res} should be a failure")

    # TODO: test DICE, HITS and FAILS with different kinds of data types
    # problem: how to change config dynamically/load dynamically?
    # at the moment it tests from the current config only

### helpers ####
def _get_ordered_injection(dice: list, lens: list) -> list:
    """Return a list of lists, which have a unique element at the second position,
    eg an amount of 4 with dice = [0,1]:
    [0,1,0,0]
    """
    if len(dice) < 2:
        raise Exception("At least two dice entries required.")
    injections = []
    # create injection
    for lst_len in lens:
        if lst_len < 1:
            injections.append([])
        else:
            injections.append([dice[0]])
            if lst_len > 1:
                injections[-1].append(dice[1])
                lst_len -= 2 # two numbers were added already
                while lst_len > 0:
                    injections[-1].append(dice[0])
                    lst_len -= 1
    return injections


if __name__ == '__main__':
    unittest.main()
